Day02 深入了解 Lazy-load 的背後實作 - Intersection Observer API


簡介 LazyLazy-loading-loading

使用 Lighthouse 檢測你開發的網頁,常會看到「Offscreen images」這個檢測指標,指的是載入網頁時花了多少額外時間載了第一屏幕畫面以外、可能不是當前最急需的檔案,這是優化 Page load time 的重點項目之一。

而 lazy-loading 技術正式想解決這個問題,如果能讓使用者資源集中花在第一屏畫面的載入,讓其他檔案都在後續需要時才載入,如下方範例中的圖片都是延遲載入的。

使用現有套件實作 lazy-loading

在網路上有許多可以達成 lazy-loading 的套件,比如這一個在 github 超過 8k 星星的知名套件( tuupola 的 lazyload),它是以 Vanilla JavaScript 實作的並只針對 image 類型做 lazy-loading。

使用方式分為以下三個部分

  1. 載入套件

    https://cdn.jsdelivr.net/npm/lazyload@2.0.0-rc.2/lazyload.js
    
  2. 將 Offscreen image 載入連結替換成 data-src 標籤,並加上 class 作為之後可以指定的 selector 目標。

    一般圖片

    <img src="<image-url>" width=400 height=400>
    

    lazy-loading 圖片

    <img class="lazyload" data-src="<image-url>" width=400 height=400>
    
  3. 在頁面載入後,針對這些 lazyload執行 lazyload 套件的

    lazyload();
    

解析 lazyload 功能背後原始碼

當執行

lazyload();

會執行以下程式

// default settings
const defaults = {
    src: "data-src",
    srcset: "data-srcset",
    selector: ".lazyload",
    root: null,
    rootMargin: "0px",
    threshold: 0
};
this.images = document.querySelectorAll(this.settings.selector);
---

let observerConfig = {
    root: this.settings.root,
    rootMargin: this.settings.rootMargin,
    threshold: [this.settings.threshold]
};

this.observer = new IntersectionObserver(function(entries) {
    Array.prototype.forEach.call(entries, function (entry) {
        if (entry.isIntersecting) {
            self.observer.unobserve(entry.target);
            let src = entry.target.getAttribute(self.settings.src);
            let srcset = entry.target.getAttribute(self.settings.srcset);
            if ("img" === entry.target.tagName.toLowerCase()) {
                if (src) {
                    entry.target.src = src;
                }
                if (srcset) {
                    entry.target.srcset = srcset;
                }
            } else {
                entry.target.style.backgroundImage = "url(" + src + ")";
            }
        }
    });
}, observerConfig);

Array.prototype.forEach.call(this.images, function (image) {
    self.observer.observe(image);
});

仔細看就會發現 IntersectionObserver這個上篇文章就有提到的技術,而此套件主要就是要把標有 lazyload 類別名稱的 lazy-loading image 們都加入 observe 的對象中,當這些元件進入畫面時就會自動觸發載入圖片的動作。

// selector =>  ".lazyload"
this.images = document.querySelectorAll(this.settings.selector);
Array.prototype.forEach.call(this.images, function (image) {
    self.observer.observe(image);
});

IntersectionObserver 第一個參數是 callback,指的就是載入圖片的,我們可以主要看到當需要載入圖片是去找得 data-src 上撰寫的圖片連結,這原本是不會被 HTML 解析的連結,將此改填入到 img 標籤的 src 上,瀏覽器立馬會去執行載入圖片的動作,是不是意外覺得簡單呢。

// src =>  "data-src"
// srcset => "data-srcset"

function(entries) {
    Array.prototype.forEach.call(entries, function (entry) {
        if (entry.isIntersecting) {
            self.observer.unobserve(entry.target);
            // 取得 data-src 之前藏放的圖片連結資料
            let src = entry.target.getAttribute(self.settings.src);
            let srcset = entry.target.getAttribute(self.settings.srcset);
            if ("img" === entry.target.tagName.toLowerCase()) {
                if (src) {
                    // 改放入到 img src 終讓頁面可以讀取 
                    entry.target.src = src;
                }
                if (srcset) {
                    entry.target.srcset = srcset;
                }
            } else {
                entry.target.style.backgroundImage = "url(" + src + ")";
            }
        }
    }
}

IntersectionObserver 第二個參數是 option(config),指的就是指定觸發的時機,引用上篇文章寫到的解說,就可以知道這個觸發時機是當 obsered 元件一出現在當前裝置畫面時就觸發 callback。

  • root 指的是監聽的區塊,填入 null 則使用預設的裝置 viewpoint,即是視覺上整個畫面
  • rootMargin 可以用來刪減監聽的區塊,使用就如同 css margin 比如想以 Nav 邊界為觸發點可以將第一個參數設定為負的 Nav Height
  • threshold 可以填入 0 ~ 1 的浮點數,在監聽元件出現比例佔達到時則觸發事件,這邊設定 0 就代表在元件「剛出現」及「剛消失」時觸發
// root => null
// rootMargin => 0px
// threshold =>  0

let observerConfig = {
    root: this.settings.root,
    rootMargin: this.settings.rootMargin,
    threshold: [this.settings.threshold]
};

看完,試著自己簡單實作一次吧

const selector = ".lazyload";
const dataSrc = "data-src";
const observerConfig = {
    root: null,
    rootMargin: '0px',
    threshold: [0]
};

const callback = function(entries, selfObserver) {
    Array.prototype.forEach.call(entries, function (entry) {
        if (entry.isIntersecting) {
            selfObserver.unobserve(entry.target);
            let src = entry.target.getAttribute(dataSrc);
            if ("img" === entry.target.tagName.toLowerCase()) {
                if (src) {
                    entry.target.src = src;
                }
            }
        }
    });
}

let $images = document.querySelectorAll(selector);
let observer = new IntersectionObserver(callback, observerConfig);

Array.prototype.forEach.call($images, function (image) {
    observer.observe(image);
});

小結

今天針對 Lazy-loading 以及實作背後使用的 Intersection Observer 做了介紹,希望大家會喜歡這類一起看原始碼類型的文章。

此外,想記錄一下其中有遺漏了一些細節,如原始碼中有寫到 srcset 這個是關於 響應式圖片載入 的實作,如果對這議題有興趣歡迎看小弟之前寫的文章

研究過程中也有看到一些 lazy-loading 相關議題,如果有興趣或有實際遇到可以在下留言一起來討論跟研究,謝謝大家。

  • 尚未載入圖片時如何設定精確 width、height,減少仔入圖片後的畫面變化過大
  • 如何設定 loading image ,甚至像 Medium 網站可以先載入一個解析度極低的圖片作為初始圖片
  • 搜尋引擎是否可以正確抓取 lazy-loading image,SEO 如何實作

參考資料

#Web #Web API #IntersectionObserver #javascript #MDN #LazyLoad







你可能感興趣的文章

1731. The Number of Employees Which Report to Each Employee

1731. The Number of Employees Which Report to Each Employee

JS30 Day 26 筆記

JS30 Day 26 筆記

F2E合作社|顏色通用類別|Bootstrap 5網頁框架開發入門

F2E合作社|顏色通用類別|Bootstrap 5網頁框架開發入門






留言討論